// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/chromeos/touch_exploration_controller.h"

#include <utility>

#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/default_tick_clock.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/window.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_tree_host.h"
#include "ui/events/event.h"
#include "ui/events/event_processor.h"
#include "ui/events/event_utils.h"
#include "ui/gfx/geometry/rect.h"

#define SET_STATE(state) SetState(state, __func__)
#define VLOG_EVENT(event) \
    if (VLOG_IS_ON(0))    \
    VlogEvent(event, __func__)

namespace ui {

namespace {

    // Delay between adjustment sounds.
    const int kSoundDelayInMS = 150;

    // In ChromeOS, VKEY_LWIN is synonymous for the search key.
    const ui::KeyboardCode kChromeOSSearchKey = ui::VKEY_LWIN;

} // namespace

TouchExplorationController::TouchExplorationController(
    aura::Window* root_window,
    TouchExplorationControllerDelegate* delegate)
    : root_window_(root_window)
    , delegate_(delegate)
    , state_(NO_FINGERS_DOWN)
    , gesture_provider_(new GestureProviderAura(this, this))
    , prev_state_(NO_FINGERS_DOWN)
    , VLOG_on_(true)
    , tick_clock_(NULL)
{
    DCHECK(root_window);
    root_window->GetHost()->GetEventSource()->AddEventRewriter(this);
    InitializeSwipeGestureMaps();
}

TouchExplorationController::~TouchExplorationController()
{
    root_window_->GetHost()->GetEventSource()->RemoveEventRewriter(this);
}

ui::EventRewriteStatus TouchExplorationController::RewriteEvent(
    const ui::Event& event,
    scoped_ptr<ui::Event>* rewritten_event)
{
    if (!event.IsTouchEvent()) {
        if (event.IsKeyEvent()) {
            const ui::KeyEvent& key_event = static_cast<const ui::KeyEvent&>(event);
            VLOG(0) << "\nKeyboard event: " << key_event.name()
                    << "\n Key code: " << key_event.key_code()
                    << ", Flags: " << key_event.flags()
                    << ", Is char: " << key_event.is_char();
        }
        return ui::EVENT_REWRITE_CONTINUE;
    }
    const ui::TouchEvent& touch_event = static_cast<const ui::TouchEvent&>(event);

    // If the tap timer should have fired by now but hasn't, run it now and
    // stop the timer. This is important so that behavior is consistent with
    // the timestamps of the events, and not dependent on the granularity of
    // the timer.
    if (tap_timer_.IsRunning() && touch_event.time_stamp() - initial_press_->time_stamp() > gesture_detector_config_.double_tap_timeout) {
        tap_timer_.Stop();
        OnTapTimerFired();
        // Note: this may change the state. We should now continue and process
        // this event under this new state.
    }

    if (passthrough_timer_.IsRunning() && event.time_stamp() - initial_press_->time_stamp() > gesture_detector_config_.longpress_timeout) {
        passthrough_timer_.Stop();
        OnPassthroughTimerFired();
    }

    const ui::EventType type = touch_event.type();
    const gfx::PointF& location = touch_event.location_f();
    const int touch_id = touch_event.touch_id();

    // Always update touch ids and touch locations, so we can use those
    // no matter what state we're in.
    if (type == ui::ET_TOUCH_PRESSED) {
        current_touch_ids_.push_back(touch_id);
        touch_locations_.insert(std::pair<int, gfx::PointF>(touch_id, location));
    } else if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
        std::vector<int>::iterator it = std::find(
            current_touch_ids_.begin(), current_touch_ids_.end(), touch_id);

        // Can happen if touch exploration is enabled while fingers were down.
        if (it == current_touch_ids_.end())
            return ui::EVENT_REWRITE_CONTINUE;

        current_touch_ids_.erase(it);
        touch_locations_.erase(touch_id);
    } else if (type == ui::ET_TOUCH_MOVED) {
        std::vector<int>::iterator it = std::find(
            current_touch_ids_.begin(), current_touch_ids_.end(), touch_id);

        // Can happen if touch exploration is enabled while fingers were down.
        if (it == current_touch_ids_.end())
            return ui::EVENT_REWRITE_CONTINUE;

        touch_locations_[*it] = location;
    } else {
        NOTREACHED() << "Unexpected event type received: " << event.name();
        return ui::EVENT_REWRITE_CONTINUE;
    }
    VLOG_EVENT(touch_event);

    // In order to avoid accidentally double tapping when moving off the edge
    // of the screen, the state will be rewritten to NoFingersDown.
    if ((type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) && FindEdgesWithinBounds(touch_event.location(), kLeavingScreenEdge) != NO_EDGE) {
        if (VLOG_on_)
            VLOG(0) << "Leaving screen";

        // Indicates to the user that they are leaving the screen.
        delegate_->PlayExitScreenEarcon();

        if (current_touch_ids_.size() == 0) {
            SET_STATE(NO_FINGERS_DOWN);
            if (VLOG_on_) {
                VLOG(0) << "Reset to no fingers in Rewrite event because the touch  "
                           "release or cancel was on the edge of the screen.";
            }
            return ui::EVENT_REWRITE_DISCARD;
        }
    }

    // If the user is in a gesture state, or if there is a possiblity that the
    // user will enter it in the future, we send the event to the gesture
    // provider so it can keep track of the state of the fingers. When the user
    // leaves one of these states, SET_STATE will set the gesture provider to
    // NULL.
    if (gesture_provider_.get()) {
        ui::TouchEvent mutable_touch_event = touch_event;
        if (gesture_provider_->OnTouchEvent(&mutable_touch_event)) {
            gesture_provider_->OnTouchEventAck(mutable_touch_event.unique_event_id(),
                false);
        }
        ProcessGestureEvents();
    }

    // The rest of the processing depends on what state we're in.
    switch (state_) {
    case NO_FINGERS_DOWN:
        return InNoFingersDown(touch_event, rewritten_event);
    case SINGLE_TAP_PRESSED:
        return InSingleTapPressed(touch_event, rewritten_event);
    case SINGLE_TAP_RELEASED:
    case TOUCH_EXPLORE_RELEASED:
        return InSingleTapOrTouchExploreReleased(touch_event, rewritten_event);
    case DOUBLE_TAP_PENDING:
        return InDoubleTapPending(touch_event, rewritten_event);
    case TOUCH_RELEASE_PENDING:
        return InTouchReleasePending(touch_event, rewritten_event);
    case TOUCH_EXPLORATION:
        return InTouchExploration(touch_event, rewritten_event);
    case GESTURE_IN_PROGRESS:
        return InGestureInProgress(touch_event, rewritten_event);
    case TOUCH_EXPLORE_SECOND_PRESS:
        return InTouchExploreSecondPress(touch_event, rewritten_event);
    case SLIDE_GESTURE:
        return InSlideGesture(touch_event, rewritten_event);
    case ONE_FINGER_PASSTHROUGH:
        return InOneFingerPassthrough(touch_event, rewritten_event);
    case CORNER_PASSTHROUGH:
        return InCornerPassthrough(touch_event, rewritten_event);
    case WAIT_FOR_NO_FINGERS:
        return InWaitForNoFingers(touch_event, rewritten_event);
    case TWO_FINGER_TAP:
        return InTwoFingerTap(touch_event, rewritten_event);
    }
    NOTREACHED();
    return ui::EVENT_REWRITE_CONTINUE;
}

ui::EventRewriteStatus TouchExplorationController::NextDispatchEvent(
    const ui::Event& last_event, scoped_ptr<ui::Event>* new_event)
{
    NOTREACHED();
    return ui::EVENT_REWRITE_CONTINUE;
}

ui::EventRewriteStatus TouchExplorationController::InNoFingersDown(
    const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event)
{
    const ui::EventType type = event.type();
    if (type != ui::ET_TOUCH_PRESSED) {
        NOTREACHED() << "Unexpected event type received: " << event.name();
        return ui::EVENT_REWRITE_CONTINUE;
    }

    // If the user enters the screen from the edge then send an earcon.
    int edge = FindEdgesWithinBounds(event.location(), kLeavingScreenEdge);
    if (edge != NO_EDGE)
        delegate_->PlayEnterScreenEarcon();

    int location = FindEdgesWithinBounds(event.location(), kSlopDistanceFromEdge);
    // If the press was at a corner, the user might go into corner passthrough
    // instead.
    bool in_a_bottom_corner = (BOTTOM_LEFT_CORNER == location) || (BOTTOM_RIGHT_CORNER == location);
    if (in_a_bottom_corner) {
        passthrough_timer_.Start(
            FROM_HERE,
            gesture_detector_config_.longpress_timeout,
            this,
            &TouchExplorationController::OnPassthroughTimerFired);
    }
    initial_press_.reset(new TouchEvent(event));
    initial_presses_[event.touch_id()] = event.location();
    last_unused_finger_event_.reset(new TouchEvent(event));
    StartTapTimer();
    SET_STATE(SINGLE_TAP_PRESSED);
    return ui::EVENT_REWRITE_DISCARD;
}

ui::EventRewriteStatus TouchExplorationController::InSingleTapPressed(
    const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event)
{
    const ui::EventType type = event.type();

    int location = FindEdgesWithinBounds(event.location(), kMaxDistanceFromEdge);
    bool in_a_bottom_corner = (location == BOTTOM_LEFT_CORNER) || (location == BOTTOM_RIGHT_CORNER);
    // If the event is from the initial press and the location is no longer in the
    // corner, then we are not waiting for a corner passthrough anymore.
    if (event.touch_id() == initial_press_->touch_id() && !in_a_bottom_corner) {
        if (passthrough_timer_.IsRunning()) {
            passthrough_timer_.Stop();
            // Since the long press timer has been running, it is possible that the
            // tap timer has timed out before the long press timer has. If the tap
            // timer timeout has elapsed, then fire the tap timer.
            if (event.time_stamp() - initial_press_->time_stamp() > gesture_detector_config_.double_tap_timeout) {
                OnTapTimerFired();
            }
        }
    }

    if (type == ui::ET_TOUCH_PRESSED) {
        initial_presses_[event.touch_id()] = event.location();
        SET_STATE(TWO_FINGER_TAP);
        return EVENT_REWRITE_DISCARD;
    } else if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
        if (passthrough_timer_.IsRunning())
            passthrough_timer_.Stop();
        if (current_touch_ids_.size() == 0 && event.touch_id() == initial_press_->touch_id()) {
            SET_STATE(SINGLE_TAP_RELEASED);
        } else if (current_touch_ids_.size() == 0) {
            SET_STATE(NO_FINGERS_DOWN);
        }
        return EVENT_REWRITE_DISCARD;
    } else if (type == ui::ET_TOUCH_MOVED) {
        float distance = (event.location() - initial_press_->location()).Length();
        // If the user does not move far enough from the original position, then the
        // resulting movement should not be considered to be a deliberate gesture or
        // touch exploration.
        if (distance <= gesture_detector_config_.touch_slop)
            return EVENT_REWRITE_DISCARD;

        float delta_time = (event.time_stamp() - initial_press_->time_stamp()).InSecondsF();
        float velocity = distance / delta_time;
        if (VLOG_on_) {
            VLOG(0) << "\n Delta time: " << delta_time << "\n Distance: " << distance
                    << "\n Velocity of click: " << velocity
                    << "\n Minimum swipe velocity: "
                    << gesture_detector_config_.minimum_swipe_velocity;
        }
        // Change to slide gesture if the slide occurred at the right edge.
        int edge = FindEdgesWithinBounds(event.location(), kMaxDistanceFromEdge);
        if (edge & RIGHT_EDGE && edge != BOTTOM_RIGHT_CORNER) {
            SET_STATE(SLIDE_GESTURE);
            return InSlideGesture(event, rewritten_event);
        }

        // If the user moves fast enough from the initial touch location, start
        // gesture detection. Otherwise, jump to the touch exploration mode early.
        if (velocity > gesture_detector_config_.minimum_swipe_velocity) {
            SET_STATE(GESTURE_IN_PROGRESS);
            return InGestureInProgress(event, rewritten_event);
        }
        EnterTouchToMouseMode();
        SET_STATE(TOUCH_EXPLORATION);
        return InTouchExploration(event, rewritten_event);
    }
    NOTREACHED();
    return ui::EVENT_REWRITE_CONTINUE;
}

ui::EventRewriteStatus
TouchExplorationController::InSingleTapOrTouchExploreReleased(
    const ui::TouchEvent& event,
    scoped_ptr<ui::Event>* rewritten_event)
{
    const ui::EventType type = event.type();
    // If there is more than one finger down, then discard to wait until no
    // fingers are down.
    if (current_touch_ids_.size() > 1) {
        SET_STATE(WAIT_FOR_NO_FINGERS);
        return ui::EVENT_REWRITE_DISCARD;
    }
    if (type == ui::ET_TOUCH_PRESSED) {
        // If there is no touch exploration yet, we can't send a click, so discard.
        if (!last_touch_exploration_) {
            tap_timer_.Stop();
            return ui::EVENT_REWRITE_DISCARD;
        }
        // This is the second tap in a double-tap (or double tap-hold).
        // We set the tap timer. If it fires before the user lifts their finger,
        // one-finger passthrough begins. Otherwise, there is a touch press and
        // release at the location of the last touch exploration.
        SET_STATE(DOUBLE_TAP_PENDING);
        // The old tap timer (from the initial click) is stopped if it is still
        // going, and the new one is set.
        tap_timer_.Stop();
        StartTapTimer();
        // This will update as the finger moves before a possible passthrough, and
        // will determine the offset.
        last_unused_finger_event_.reset(new ui::TouchEvent(event));
        return ui::EVENT_REWRITE_DISCARD;
    } else if (type == ui::ET_TOUCH_RELEASED && !last_touch_exploration_) {
        // If the previous press was discarded, we need to also handle its
        // release.
        if (current_touch_ids_.size() == 0) {
            SET_STATE(NO_FINGERS_DOWN);
        }
        return ui::EVENT_REWRITE_DISCARD;
    } else if (type == ui::ET_TOUCH_MOVED) {
        return ui::EVENT_REWRITE_DISCARD;
    }
    NOTREACHED();
    return ui::EVENT_REWRITE_CONTINUE;
}

ui::EventRewriteStatus TouchExplorationController::InDoubleTapPending(
    const ui::TouchEvent& event,
    scoped_ptr<ui::Event>* rewritten_event)
{
    const ui::EventType type = event.type();
    if (type == ui::ET_TOUCH_PRESSED) {
        return ui::EVENT_REWRITE_DISCARD;
    } else if (type == ui::ET_TOUCH_MOVED) {
        // If the user moves far enough from the initial touch location (outside
        // the "slop" region, jump to passthrough mode early.
        float delta = (event.location() - initial_press_->location()).Length();
        if (delta > gesture_detector_config_.touch_slop) {
            tap_timer_.Stop();
            OnTapTimerFired();
        }
        return EVENT_REWRITE_DISCARD;
    } else if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
        if (current_touch_ids_.size() != 0)
            return EVENT_REWRITE_DISCARD;

        scoped_ptr<ui::TouchEvent> touch_press;
        touch_press.reset(new ui::TouchEvent(ui::ET_TOUCH_PRESSED, gfx::Point(),
            initial_press_->touch_id(),
            event.time_stamp()));
        touch_press->set_location_f(last_touch_exploration_->location_f());
        touch_press->set_root_location_f(last_touch_exploration_->location_f());
        DispatchEvent(touch_press.get());

        scoped_ptr<ui::TouchEvent> new_event(
            new ui::TouchEvent(ui::ET_TOUCH_RELEASED, gfx::Point(),
                initial_press_->touch_id(), event.time_stamp()));
        new_event->set_location_f(last_touch_exploration_->location_f());
        new_event->set_root_location_f(last_touch_exploration_->location_f());
        new_event->set_flags(event.flags());
        *rewritten_event = std::move(new_event);
        SET_STATE(NO_FINGERS_DOWN);
        return ui::EVENT_REWRITE_REWRITTEN;
    }
    NOTREACHED();
    return ui::EVENT_REWRITE_CONTINUE;
}

ui::EventRewriteStatus TouchExplorationController::InTouchReleasePending(
    const ui::TouchEvent& event,
    scoped_ptr<ui::Event>* rewritten_event)
{
    const ui::EventType type = event.type();
    if (type == ui::ET_TOUCH_PRESSED || type == ui::ET_TOUCH_MOVED) {
        return ui::EVENT_REWRITE_DISCARD;
    } else if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
        if (current_touch_ids_.size() != 0)
            return EVENT_REWRITE_DISCARD;

        scoped_ptr<ui::TouchEvent> new_event(
            new ui::TouchEvent(ui::ET_TOUCH_RELEASED, gfx::Point(),
                initial_press_->touch_id(), event.time_stamp()));
        new_event->set_location_f(last_touch_exploration_->location_f());
        new_event->set_root_location_f(last_touch_exploration_->location_f());
        new_event->set_flags(event.flags());
        *rewritten_event = std::move(new_event);
        SET_STATE(NO_FINGERS_DOWN);
        return ui::EVENT_REWRITE_REWRITTEN;
    }
    NOTREACHED();
    return ui::EVENT_REWRITE_CONTINUE;
}

ui::EventRewriteStatus TouchExplorationController::InTouchExploration(
    const ui::TouchEvent& event,
    scoped_ptr<ui::Event>* rewritten_event)
{
    const ui::EventType type = event.type();
    if (type == ui::ET_TOUCH_PRESSED) {
        // Handle split-tap.
        initial_press_.reset(new TouchEvent(event));
        tap_timer_.Stop();
        scoped_ptr<ui::TouchEvent> new_event(
            new ui::TouchEvent(ui::ET_TOUCH_PRESSED, gfx::Point(), event.touch_id(),
                event.time_stamp()));
        new_event->set_location_f(last_touch_exploration_->location_f());
        new_event->set_root_location_f(last_touch_exploration_->location_f());
        new_event->set_flags(event.flags());
        *rewritten_event = std::move(new_event);
        SET_STATE(TOUCH_EXPLORE_SECOND_PRESS);
        return ui::EVENT_REWRITE_REWRITTEN;
    } else if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
        initial_press_.reset(new TouchEvent(event));
        StartTapTimer();
        SET_STATE(TOUCH_EXPLORE_RELEASED);
    } else if (type != ui::ET_TOUCH_MOVED) {
        NOTREACHED();
        return ui::EVENT_REWRITE_CONTINUE;
    }

    // Rewrite as a mouse-move event.
    *rewritten_event = CreateMouseMoveEvent(event.location_f(), event.flags());
    last_touch_exploration_.reset(new TouchEvent(event));
    return ui::EVENT_REWRITE_REWRITTEN;
}

ui::EventRewriteStatus TouchExplorationController::InGestureInProgress(
    const ui::TouchEvent& event,
    scoped_ptr<ui::Event>* rewritten_event)
{
    // The events were sent to the gesture provider in RewriteEvent already.
    // If no gesture is registered before the tap timer times out, the state
    // will change to "wait for no fingers down" or "touch exploration" depending
    // on the number of fingers down, and this function will stop being called.
    if (current_touch_ids_.size() == 0) {
        SET_STATE(NO_FINGERS_DOWN);
    }
    return ui::EVENT_REWRITE_DISCARD;
}

ui::EventRewriteStatus TouchExplorationController::InCornerPassthrough(
    const ui::TouchEvent& event,
    scoped_ptr<ui::Event>* rewritten_event)
{
    ui::EventType type = event.type();

    // If the first finger has left the corner, then exit passthrough.
    if (event.touch_id() == initial_press_->touch_id()) {
        int edges = FindEdgesWithinBounds(event.location(), kSlopDistanceFromEdge);
        bool in_a_bottom_corner = (edges == BOTTOM_LEFT_CORNER) || (edges == BOTTOM_RIGHT_CORNER);
        if (type == ui::ET_TOUCH_MOVED && in_a_bottom_corner)
            return ui::EVENT_REWRITE_DISCARD;

        if (current_touch_ids_.size() == 0) {
            SET_STATE(NO_FINGERS_DOWN);
            return ui::EVENT_REWRITE_DISCARD;
        }
        SET_STATE(WAIT_FOR_NO_FINGERS);
        return ui::EVENT_REWRITE_DISCARD;
    }

    scoped_ptr<ui::TouchEvent> new_event(new ui::TouchEvent(
        type, gfx::Point(), event.touch_id(), event.time_stamp()));
    new_event->set_location_f(event.location_f());
    new_event->set_root_location_f(event.location_f());
    new_event->set_flags(event.flags());
    *rewritten_event = std::move(new_event);

    if (current_touch_ids_.size() == 0)
        SET_STATE(NO_FINGERS_DOWN);

    return ui::EVENT_REWRITE_REWRITTEN;
}

ui::EventRewriteStatus TouchExplorationController::InOneFingerPassthrough(
    const ui::TouchEvent& event,
    scoped_ptr<ui::Event>* rewritten_event)
{
    if (event.touch_id() != initial_press_->touch_id()) {
        if (current_touch_ids_.size() == 0) {
            SET_STATE(NO_FINGERS_DOWN);
        }
        return ui::EVENT_REWRITE_DISCARD;
    }
    scoped_ptr<ui::TouchEvent> new_event(new ui::TouchEvent(
        event.type(), gfx::Point(), event.touch_id(), event.time_stamp()));
    new_event->set_location_f(event.location_f() - passthrough_offset_);
    new_event->set_root_location_f(event.location_f() - passthrough_offset_);
    new_event->set_flags(event.flags());
    *rewritten_event = std::move(new_event);
    if (current_touch_ids_.size() == 0) {
        SET_STATE(NO_FINGERS_DOWN);
    }
    return ui::EVENT_REWRITE_REWRITTEN;
}

ui::EventRewriteStatus TouchExplorationController::InTouchExploreSecondPress(
    const ui::TouchEvent& event,
    scoped_ptr<ui::Event>* rewritten_event)
{
    ui::EventType type = event.type();
    gfx::PointF location = event.location_f();
    if (type == ui::ET_TOUCH_PRESSED) {
        // A third finger being pressed means that a split tap can no longer go
        // through. The user enters the wait state, Since there has already been
        // a press dispatched when split tap began, the touch needs to be
        // cancelled.
        scoped_ptr<ui::TouchEvent> new_event(
            new ui::TouchEvent(ui::ET_TOUCH_CANCELLED, gfx::Point(),
                initial_press_->touch_id(), event.time_stamp()));
        new_event->set_location_f(last_touch_exploration_->location_f());
        new_event->set_root_location_f(last_touch_exploration_->location_f());
        new_event->set_flags(event.flags());
        *rewritten_event = std::move(new_event);
        SET_STATE(WAIT_FOR_NO_FINGERS);
        return ui::EVENT_REWRITE_REWRITTEN;
    } else if (type == ui::ET_TOUCH_MOVED) {
        // If the fingers have moved too far from their original locations,
        // the user can no longer split tap.
        ui::TouchEvent* original_touch;
        if (event.touch_id() == last_touch_exploration_->touch_id()) {
            original_touch = last_touch_exploration_.get();
        } else if (event.touch_id() == initial_press_->touch_id()) {
            original_touch = initial_press_.get();
        } else {
            NOTREACHED();
            SET_STATE(WAIT_FOR_NO_FINGERS);
            return ui::EVENT_REWRITE_DISCARD;
        }
        // Check the distance between the current finger location and the original
        // location. The slop for this is a bit more generous since keeping two
        // fingers in place is a bit harder. If the user has left the slop, the
        // split tap press (which was previous dispatched) is lifted with a touch
        // cancelled, and the user enters the wait state.
        if ((event.location_f() - original_touch->location_f()).Length() > GetSplitTapTouchSlop()) {
            scoped_ptr<ui::TouchEvent> new_event(
                new ui::TouchEvent(ui::ET_TOUCH_CANCELLED, gfx::Point(),
                    initial_press_->touch_id(), event.time_stamp()));
            new_event->set_location_f(last_touch_exploration_->location_f());
            new_event->set_root_location_f(last_touch_exploration_->location_f());
            new_event->set_flags(event.flags());
            *rewritten_event = std::move(new_event);
            SET_STATE(WAIT_FOR_NO_FINGERS);
            return ui::EVENT_REWRITE_REWRITTEN;
        }
        return ui::EVENT_REWRITE_DISCARD;
    } else if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
        // If the touch exploration finger is lifted, there is no option to return
        // to touch explore anymore. The remaining finger acts as a pending
        // tap or long tap for the last touch explore location.
        if (event.touch_id() == last_touch_exploration_->touch_id()) {
            SET_STATE(TOUCH_RELEASE_PENDING);
            return EVENT_REWRITE_DISCARD;
        }

        // Continue to release the touch only if the touch explore finger is the
        // only finger remaining.
        if (current_touch_ids_.size() != 1)
            return EVENT_REWRITE_DISCARD;

        // Rewrite at location of last touch exploration.
        scoped_ptr<ui::TouchEvent> new_event(
            new ui::TouchEvent(ui::ET_TOUCH_RELEASED, gfx::Point(),
                initial_press_->touch_id(), event.time_stamp()));
        new_event->set_location_f(last_touch_exploration_->location_f());
        new_event->set_root_location_f(last_touch_exploration_->location_f());
        new_event->set_flags(event.flags());
        *rewritten_event = std::move(new_event);
        SET_STATE(TOUCH_EXPLORATION);
        EnterTouchToMouseMode();
        return ui::EVENT_REWRITE_REWRITTEN;
    }
    NOTREACHED();
    return ui::EVENT_REWRITE_CONTINUE;
}

ui::EventRewriteStatus TouchExplorationController::InWaitForNoFingers(
    const ui::TouchEvent& event,
    scoped_ptr<ui::Event>* rewritten_event)
{
    if (current_touch_ids_.size() == 0)
        SET_STATE(NO_FINGERS_DOWN);
    return EVENT_REWRITE_DISCARD;
}

void TouchExplorationController::PlaySoundForTimer()
{
    delegate_->PlayVolumeAdjustEarcon();
}

ui::EventRewriteStatus TouchExplorationController::InSlideGesture(
    const ui::TouchEvent& event,
    scoped_ptr<ui::Event>* rewritten_event)
{
    // The timer should not fire when sliding.
    tap_timer_.Stop();

    ui::EventType type = event.type();
    // If additional fingers are added before a swipe gesture has been registered,
    // then wait until all fingers have been lifted.
    if (type == ui::ET_TOUCH_PRESSED || event.touch_id() != initial_press_->touch_id()) {
        if (sound_timer_.IsRunning())
            sound_timer_.Stop();
        SET_STATE(WAIT_FOR_NO_FINGERS);
        return EVENT_REWRITE_DISCARD;
    }

    // There should not be more than one finger down.
    DCHECK_LE(current_touch_ids_.size(), 1U);

    // Allows user to return to the edge to adjust the sound if they have left the
    // boundaries.
    int edge = FindEdgesWithinBounds(event.location(), kSlopDistanceFromEdge);
    if (!(edge & RIGHT_EDGE) && (type != ui::ET_TOUCH_RELEASED)) {
        if (sound_timer_.IsRunning()) {
            sound_timer_.Stop();
        }
        return EVENT_REWRITE_DISCARD;
    }

    // This can occur if the user leaves the screen edge and then returns to it to
    // continue adjusting the sound.
    if (!sound_timer_.IsRunning()) {
        sound_timer_.Start(FROM_HERE,
            base::TimeDelta::FromMilliseconds(kSoundDelayInMS), this,
            &ui::TouchExplorationController::PlaySoundForTimer);
        delegate_->PlayVolumeAdjustEarcon();
    }

    if (current_touch_ids_.size() == 0) {
        SET_STATE(NO_FINGERS_DOWN);
    }
    return ui::EVENT_REWRITE_DISCARD;
}

ui::EventRewriteStatus TouchExplorationController::InTwoFingerTap(
    const ui::TouchEvent& event,
    scoped_ptr<ui::Event>* rewritten_event)
{
    ui::EventType type = event.type();
    if (type == ui::ET_TOUCH_PRESSED) {
        // This is now a three finger gesture.
        SET_STATE(GESTURE_IN_PROGRESS);
        return ui::EVENT_REWRITE_DISCARD;
    }

    if (type == ui::ET_TOUCH_MOVED) {
        // Determine if it was a swipe.
        gfx::Point original_location = initial_presses_[event.touch_id()];
        float distance = (event.location() - original_location).Length();
        // If the user moves too far from the original position, consider the
        // movement a swipe.
        if (distance > gesture_detector_config_.touch_slop) {
            SET_STATE(GESTURE_IN_PROGRESS);
        }
        return ui::EVENT_REWRITE_DISCARD;
    }

    if (current_touch_ids_.size() != 0)
        return ui::EVENT_REWRITE_DISCARD;

    if (type == ui::ET_TOUCH_RELEASED) {
        // In ChromeVox, pressing control will stop ChromeVox from speaking.
        ui::KeyEvent control_down(
            ui::ET_KEY_PRESSED, ui::VKEY_CONTROL, ui::EF_CONTROL_DOWN);
        ui::KeyEvent control_up(ui::ET_KEY_RELEASED, ui::VKEY_CONTROL, ui::EF_NONE);

        DispatchEvent(&control_down);
        DispatchEvent(&control_up);
        SET_STATE(NO_FINGERS_DOWN);
        return ui::EVENT_REWRITE_DISCARD;
    }
    return ui::EVENT_REWRITE_DISCARD;
}

base::TimeDelta TouchExplorationController::Now()
{
    if (tick_clock_) {
        // This is the same as what EventTimeForNow() does, but here we do it
        // with a clock that can be replaced with a simulated clock for tests.
        return base::TimeDelta::FromInternalValue(
            tick_clock_->NowTicks().ToInternalValue());
    }
    return ui::EventTimeForNow();
}

void TouchExplorationController::StartTapTimer()
{
    tap_timer_.Start(FROM_HERE,
        gesture_detector_config_.double_tap_timeout,
        this,
        &TouchExplorationController::OnTapTimerFired);
}

void TouchExplorationController::OnTapTimerFired()
{
    switch (state_) {
    case SINGLE_TAP_RELEASED:
        SET_STATE(NO_FINGERS_DOWN);
        break;
    case TOUCH_EXPLORE_RELEASED:
        SET_STATE(NO_FINGERS_DOWN);
        last_touch_exploration_.reset(new TouchEvent(*initial_press_));
        return;
    case DOUBLE_TAP_PENDING: {
        SET_STATE(ONE_FINGER_PASSTHROUGH);
        passthrough_offset_ = last_unused_finger_event_->location_f() - last_touch_exploration_->location_f();
        scoped_ptr<ui::TouchEvent> passthrough_press(
            new ui::TouchEvent(ui::ET_TOUCH_PRESSED, gfx::Point(),
                last_unused_finger_event_->touch_id(), Now()));
        passthrough_press->set_location_f(last_touch_exploration_->location_f());
        passthrough_press->set_root_location_f(
            last_touch_exploration_->location_f());
        DispatchEvent(passthrough_press.get());
        return;
    }
    case SINGLE_TAP_PRESSED:
        if (passthrough_timer_.IsRunning())
            return;
    case GESTURE_IN_PROGRESS:
        // If only one finger is down, go into touch exploration.
        if (current_touch_ids_.size() == 1) {
            EnterTouchToMouseMode();
            SET_STATE(TOUCH_EXPLORATION);
            break;
        }
        // Otherwise wait for all fingers to be lifted.
        SET_STATE(WAIT_FOR_NO_FINGERS);
        return;
    case TWO_FINGER_TAP:
        SET_STATE(WAIT_FOR_NO_FINGERS);
        break;
    default:
        return;
    }
    EnterTouchToMouseMode();
    scoped_ptr<ui::Event> mouse_move = CreateMouseMoveEvent(
        initial_press_->location_f(), initial_press_->flags());
    DispatchEvent(mouse_move.get());
    last_touch_exploration_.reset(new TouchEvent(*initial_press_));
}

void TouchExplorationController::OnPassthroughTimerFired()
{
    // The passthrough timer will only fire if if the user has held a finger in
    // one of the passthrough corners for the duration of the passthrough timeout.

    // Check that initial press isn't null. Also a check that if the initial
    // corner press was released, then it should not be in corner passthrough.
    if (!initial_press_ || touch_locations_.find(initial_press_->touch_id()) != touch_locations_.end()) {
        LOG(ERROR) << "No initial press or the initial press has been released.";
    }

    gfx::Point location = ToRoundedPoint(touch_locations_[initial_press_->touch_id()]);
    int corner = FindEdgesWithinBounds(location, kSlopDistanceFromEdge);
    if (corner != BOTTOM_LEFT_CORNER && corner != BOTTOM_RIGHT_CORNER)
        return;

    if (sound_timer_.IsRunning())
        sound_timer_.Stop();
    delegate_->PlayPassthroughEarcon();
    SET_STATE(CORNER_PASSTHROUGH);
    return;
}

void TouchExplorationController::DispatchEvent(ui::Event* event)
{
    ignore_result(
        root_window_->GetHost()->dispatcher()->OnEventFromSource(event));
}

// This is an override for a function that is only called for timer-based events
// like long press. Events that are created synchronously as a result of
// certain touch events are added to the vector accessible via
// GetAndResetPendingGestures(). We only care about swipes (which are created
// synchronously), so we ignore this callback.
void TouchExplorationController::OnGestureEvent(ui::GestureConsumer* consumer,
    ui::GestureEvent* gesture) { }

void TouchExplorationController::ProcessGestureEvents()
{
    scoped_ptr<ScopedVector<ui::GestureEvent>> gestures(
        gesture_provider_->GetAndResetPendingGestures());
    if (gestures) {
        for (ScopedVector<GestureEvent>::iterator i = gestures->begin();
             i != gestures->end();
             ++i) {
            if ((*i)->type() == ui::ET_GESTURE_SWIPE && state_ == GESTURE_IN_PROGRESS) {
                OnSwipeEvent(*i);
                // The tap timer to leave gesture state is ended, and we now wait for
                // all fingers to be released.
                tap_timer_.Stop();
                SET_STATE(WAIT_FOR_NO_FINGERS);
                return;
            }
            if (state_ == SLIDE_GESTURE && (*i)->IsScrollGestureEvent()) {
                SideSlideControl(*i);
            }
        }
    }
}

void TouchExplorationController::SideSlideControl(ui::GestureEvent* gesture)
{
    ui::EventType type = gesture->type();

    if (type == ET_GESTURE_SCROLL_BEGIN) {
        delegate_->PlayVolumeAdjustEarcon();
    }

    if (type == ET_GESTURE_SCROLL_END) {
        if (sound_timer_.IsRunning())
            sound_timer_.Stop();
        delegate_->PlayVolumeAdjustEarcon();
    }

    // If the user is in the corner of the right side of the screen, the volume
    // will be automatically set to 100% or muted depending on which corner they
    // are in. Otherwise, the user will be able to adjust the volume by sliding
    // their finger along the right side of the screen. Volume is relative to
    // where they are on the right side of the screen.
    gfx::Point location = gesture->location();
    int edge = FindEdgesWithinBounds(location, kSlopDistanceFromEdge);
    if (!(edge & RIGHT_EDGE))
        return;

    if (edge & TOP_EDGE) {
        delegate_->SetOutputLevel(100);
        return;
    }
    if (edge & BOTTOM_EDGE) {
        delegate_->SetOutputLevel(0);
        return;
    }

    location = gesture->location();
    root_window_->GetHost()->ConvertPointFromNativeScreen(&location);
    float volume_adjust_height = root_window_->bounds().height() - 2 * kMaxDistanceFromEdge;
    float ratio = (location.y() - kMaxDistanceFromEdge) / volume_adjust_height;
    float volume = 100 - 100 * ratio;
    if (VLOG_on_) {
        VLOG(0) << "\n Volume = " << volume
                << "\n Location = " << location.ToString()
                << "\n Bounds = " << root_window_->bounds().right();
    }
    delegate_->SetOutputLevel(static_cast<int>(volume));
}

void TouchExplorationController::OnSwipeEvent(ui::GestureEvent* swipe_gesture)
{
    // A swipe gesture contains details for the direction in which the swipe
    // occurred. TODO(evy) : Research which swipe results users most want and
    // remap these swipes to the best events. Hopefully in the near future
    // there will also be a menu for users to pick custom mappings.
    GestureEventDetails event_details = swipe_gesture->details();
    int num_fingers = event_details.touch_points();
    if (VLOG_on_)
        VLOG(0) << "\nSwipe with " << num_fingers << " fingers.";

    if (num_fingers > 4)
        return;

    if (event_details.swipe_left() && !left_swipe_gestures_[num_fingers].is_null()) {
        left_swipe_gestures_[num_fingers].Run();
    } else if (event_details.swipe_right() && !right_swipe_gestures_[num_fingers].is_null()) {
        right_swipe_gestures_[num_fingers].Run();
    } else if (event_details.swipe_up() && !up_swipe_gestures_[num_fingers].is_null()) {
        up_swipe_gestures_[num_fingers].Run();
    } else if (event_details.swipe_down() && !down_swipe_gestures_[num_fingers].is_null()) {
        down_swipe_gestures_[num_fingers].Run();
    }
}

int TouchExplorationController::FindEdgesWithinBounds(gfx::Point point,
    float bounds)
{
    // Since GetBoundsInScreen is in DIPs but point is not, then point needs to be
    // converted.
    root_window_->GetHost()->ConvertPointFromNativeScreen(&point);
    gfx::Rect window = root_window_->GetBoundsInScreen();

    float left_edge_limit = window.x() + bounds;
    float right_edge_limit = window.right() - bounds;
    float top_edge_limit = window.y() + bounds;
    float bottom_edge_limit = window.bottom() - bounds;

    // Bitwise manipulation in order to determine where on the screen the point
    // lies. If more than one bit is turned on, then it is a corner where the two
    // bit/edges intersect. Otherwise, if no bits are turned on, the point must be
    // in the center of the screen.
    int result = NO_EDGE;
    if (point.x() < left_edge_limit)
        result |= LEFT_EDGE;
    if (point.x() > right_edge_limit)
        result |= RIGHT_EDGE;
    if (point.y() < top_edge_limit)
        result |= TOP_EDGE;
    if (point.y() > bottom_edge_limit)
        result |= BOTTOM_EDGE;
    return result;
}

void TouchExplorationController::DispatchShiftSearchKeyEvent(
    const ui::KeyboardCode third_key)
{
    // In order to activate the shortcut shift+search+<arrow key>
    // three KeyPressed events must be dispatched in succession along
    // with three KeyReleased events.

    ui::KeyEvent shift_down(
        ui::ET_KEY_PRESSED, ui::VKEY_SHIFT, ui::EF_SHIFT_DOWN);
    ui::KeyEvent search_down(
        ui::ET_KEY_PRESSED, kChromeOSSearchKey, ui::EF_SHIFT_DOWN);
    ui::KeyEvent third_key_down(ui::ET_KEY_PRESSED, third_key, ui::EF_SHIFT_DOWN);

    ui::KeyEvent third_key_up(ui::ET_KEY_RELEASED, third_key, ui::EF_SHIFT_DOWN);
    ui::KeyEvent search_up(
        ui::ET_KEY_RELEASED, kChromeOSSearchKey, ui::EF_SHIFT_DOWN);
    ui ::KeyEvent shift_up(ui::ET_KEY_RELEASED, ui::VKEY_SHIFT, ui::EF_NONE);

    DispatchEvent(&shift_down);
    DispatchEvent(&search_down);
    DispatchEvent(&third_key_down);
    DispatchEvent(&third_key_up);
    DispatchEvent(&search_up);
    DispatchEvent(&shift_up);
}

base::Closure TouchExplorationController::BindShiftSearchKeyEvent(
    const ui::KeyboardCode third_key)
{
    return base::Bind(&TouchExplorationController::DispatchShiftSearchKeyEvent,
        base::Unretained(this),
        third_key);
}

void TouchExplorationController::DispatchKeyWithFlags(
    const ui::KeyboardCode key,
    int flags)
{
    ui::KeyEvent key_down(ui::ET_KEY_PRESSED, key, flags);
    ui::KeyEvent key_up(ui::ET_KEY_RELEASED, key, flags);
    DispatchEvent(&key_down);
    DispatchEvent(&key_up);
    if (VLOG_on_) {
        VLOG(0) << "\nKey down: key code : " << key_down.key_code()
                << ", flags: " << key_down.flags()
                << "\nKey up: key code : " << key_up.key_code()
                << ", flags: " << key_up.flags();
    }
}

base::Closure TouchExplorationController::BindKeyEventWithFlags(
    const ui::KeyboardCode key,
    int flags)
{
    return base::Bind(&TouchExplorationController::DispatchKeyWithFlags,
        base::Unretained(this),
        key,
        flags);
}

scoped_ptr<ui::MouseEvent> TouchExplorationController::CreateMouseMoveEvent(
    const gfx::PointF& location,
    int flags)
{
    // The "synthesized" flag should be set on all events that don't have a
    // backing native event.
    flags |= ui::EF_IS_SYNTHESIZED;

    // This flag is used to identify mouse move events that were generated from
    // touch exploration in Chrome code.
    flags |= ui::EF_TOUCH_ACCESSIBILITY;

    // TODO(dmazzoni) http://crbug.com/391008 - get rid of this hack.
    // This is a short-term workaround for the limitation that we're using
    // the ChromeVox content script to process touch exploration events, but
    // ChromeVox needs a way to distinguish between a real mouse move and a
    // mouse move generated from touch exploration, so we have touch exploration
    // pretend that the command key was down (which becomes the "meta" key in
    // JavaScript). We can remove this hack when the ChromeVox content script
    // goes away and native accessibility code sends a touch exploration
    // event to the new ChromeVox background page via the automation api.
    flags |= ui::EF_COMMAND_DOWN;

    scoped_ptr<ui::MouseEvent> event(
        new ui::MouseEvent(ui::ET_MOUSE_MOVED, gfx::Point(), gfx::Point(),
            ui::EventTimeForNow(), flags, 0));
    event->set_location_f(location);
    event->set_root_location_f(location);
    return event;
}

void TouchExplorationController::EnterTouchToMouseMode()
{
    aura::client::CursorClient* cursor_client = aura::client::GetCursorClient(root_window_);
    if (cursor_client && !cursor_client->IsMouseEventsEnabled())
        cursor_client->EnableMouseEvents();
    if (cursor_client && cursor_client->IsCursorVisible())
        cursor_client->HideCursor();
}

void TouchExplorationController::SetState(State new_state,
    const char* function_name)
{
    state_ = new_state;
    VlogState(function_name);
    // These are the states the user can be in that will never result in a
    // gesture before the user returns to NO_FINGERS_DOWN. Therefore, if the
    // gesture provider still exists, it's reset to NULL until the user returns
    // to NO_FINGERS_DOWN.
    switch (new_state) {
    case SINGLE_TAP_RELEASED:
    case TOUCH_EXPLORE_RELEASED:
    case DOUBLE_TAP_PENDING:
    case TOUCH_RELEASE_PENDING:
    case TOUCH_EXPLORATION:
    case TOUCH_EXPLORE_SECOND_PRESS:
    case ONE_FINGER_PASSTHROUGH:
    case CORNER_PASSTHROUGH:
    case WAIT_FOR_NO_FINGERS:
        if (gesture_provider_.get())
            gesture_provider_.reset(NULL);
        break;
    case NO_FINGERS_DOWN:
        gesture_provider_.reset(new GestureProviderAura(this, this));
        if (sound_timer_.IsRunning())
            sound_timer_.Stop();
        tap_timer_.Stop();
        break;
    case SINGLE_TAP_PRESSED:
    case GESTURE_IN_PROGRESS:
    case SLIDE_GESTURE:
    case TWO_FINGER_TAP:
        break;
    }
}

void TouchExplorationController::VlogState(const char* function_name)
{
    if (!VLOG_on_)
        return;
    if (prev_state_ == state_)
        return;
    prev_state_ = state_;
    const char* state_string = EnumStateToString(state_);
    VLOG(0) << "\n Function name: " << function_name
            << "\n State: " << state_string;
}

void TouchExplorationController::VlogEvent(const ui::TouchEvent& touch_event,
    const char* function_name)
{
    if (!VLOG_on_)
        return;

    if (prev_event_ && prev_event_->type() == touch_event.type() && prev_event_->touch_id() == touch_event.touch_id()) {
        return;
    }
    // The above statement prevents events of the same type and id from being
    // printed in a row. However, if two fingers are down, they would both be
    // moving and alternating printing move events unless we check for this.
    if (prev_event_ && prev_event_->type() == ET_TOUCH_MOVED && touch_event.type() == ET_TOUCH_MOVED) {
        return;
    }

    const std::string& type = touch_event.name();
    const gfx::PointF& location = touch_event.location_f();
    const int touch_id = touch_event.touch_id();

    VLOG(0) << "\n Function name: " << function_name
            << "\n Event Type: " << type
            << "\n Location: " << location.ToString()
            << "\n Touch ID: " << touch_id;
    prev_event_.reset(new TouchEvent(touch_event));
}

const char* TouchExplorationController::EnumStateToString(State state)
{
    switch (state) {
    case NO_FINGERS_DOWN:
        return "NO_FINGERS_DOWN";
    case SINGLE_TAP_PRESSED:
        return "SINGLE_TAP_PRESSED";
    case SINGLE_TAP_RELEASED:
        return "SINGLE_TAP_RELEASED";
    case TOUCH_EXPLORE_RELEASED:
        return "TOUCH_EXPLORE_RELEASED";
    case DOUBLE_TAP_PENDING:
        return "DOUBLE_TAP_PENDING";
    case TOUCH_RELEASE_PENDING:
        return "TOUCH_RELEASE_PENDING";
    case TOUCH_EXPLORATION:
        return "TOUCH_EXPLORATION";
    case GESTURE_IN_PROGRESS:
        return "GESTURE_IN_PROGRESS";
    case TOUCH_EXPLORE_SECOND_PRESS:
        return "TOUCH_EXPLORE_SECOND_PRESS";
    case CORNER_PASSTHROUGH:
        return "CORNER_PASSTHROUGH";
    case SLIDE_GESTURE:
        return "SLIDE_GESTURE";
    case ONE_FINGER_PASSTHROUGH:
        return "ONE_FINGER_PASSTHROUGH";
    case WAIT_FOR_NO_FINGERS:
        return "WAIT_FOR_NO_FINGERS";
    case TWO_FINGER_TAP:
        return "TWO_FINGER_TAP";
    }
    return "Not a state";
}

// TODO(evy, lisayin) : Just call abstracted methods on the delegate (e.g.
// Swipe(Direction direction, int num_fingers)), and add the DispatchXYZ
// methods to the delegate. Avoid the middle step of dispatching keys at all,
// and simply have ChromeVox/ChromeOS complete the required action.

void TouchExplorationController::InitializeSwipeGestureMaps()
{
    // Gestures with one finger are used for navigation.
    left_swipe_gestures_[1] = BindShiftSearchKeyEvent(ui::VKEY_LEFT);
    right_swipe_gestures_[1] = BindShiftSearchKeyEvent(ui::VKEY_RIGHT);
    up_swipe_gestures_[1] = BindShiftSearchKeyEvent(ui::VKEY_UP);
    down_swipe_gestures_[1] = BindShiftSearchKeyEvent(ui::VKEY_DOWN);

    // Gestures with two fingers.
    left_swipe_gestures_[2] = BindKeyEventWithFlags(ui::VKEY_BROWSER_BACK, ui::EF_NONE);
    right_swipe_gestures_[2] = BindKeyEventWithFlags(ui::VKEY_BROWSER_FORWARD, ui::EF_NONE);
    // Jump to top.
    up_swipe_gestures_[2] = BindShiftSearchKeyEvent(ui::VKEY_A);
    // Read from here.
    down_swipe_gestures_[2] = BindShiftSearchKeyEvent(ui::VKEY_R);

    // Gestures with three fingers switch tabs left/right and scroll up/down.
    left_swipe_gestures_[3] = BindKeyEventWithFlags(
        ui::VKEY_TAB, ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN);
    right_swipe_gestures_[3] = BindKeyEventWithFlags(ui::VKEY_TAB, ui::EF_CONTROL_DOWN);
    up_swipe_gestures_[3] = BindKeyEventWithFlags(ui::VKEY_NEXT, ui::EF_NONE);
    down_swipe_gestures_[3] = BindKeyEventWithFlags(ui::VKEY_PRIOR, ui::EF_NONE);

    // Gestures with four fingers should probably eventually be used for rare
    // needs that are hard to access through menus.
    // Note that brightness levels are here because they can be important for low
    // vision users. However, none of these mappings are permanent.
    left_swipe_gestures_[4] = BindKeyEventWithFlags(ui::VKEY_BRIGHTNESS_DOWN, ui::EF_NONE);
    right_swipe_gestures_[4] = BindKeyEventWithFlags(VKEY_BRIGHTNESS_UP, ui::EF_NONE);
    up_swipe_gestures_[4] = BindKeyEventWithFlags(VKEY_BROWSER_HOME, ui::EF_NONE);
    down_swipe_gestures_[4] = BindKeyEventWithFlags(VKEY_BROWSER_REFRESH, ui::EF_NONE);
}

float TouchExplorationController::GetSplitTapTouchSlop()
{
    return gesture_detector_config_.touch_slop * 3;
}

} // namespace ui
